package io.summer.basis.log;

import io.summer.basis.core.MyServletRequestWrapper;
import io.summer.basis.tool.IpTool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.util.ContentCachingResponseWrapper;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
import java.util.StringJoiner;

/**
 * maybe better to use ResponseBodyAdvice, RequestBodyAdvice in spring boot!
 *
 * @author Moonlight
 */
@Slf4j
public class LogFilter implements Filter {
  enum LogType {
    /**
     * default
     */
    NONE,
    /**
     * merge request and response
     */
    MERGE
  }

  private final LogFilterProperties logFilterProperties;

  public LogFilter(LogFilterProperties properties) {
    logFilterProperties = properties;
  }

  private void appendLog(StringBuilder sb, String value) {
    sb.append(value).append(logFilterProperties.getSeparator());
  }

  String buildRequestLog(ServletRequest request, boolean isHttpRequest) throws IOException {
    StringBuilder sb = new StringBuilder(512);
    appendLog(sb, "Request>>>>>>");
    String ip = "";
    if (isHttpRequest) {
      HttpServletRequest httpRequest = (HttpServletRequest) request;
      appendLog(sb, "url:" + httpRequest.getRequestURL());
      appendLog(sb, "method:" + httpRequest.getMethod());
      appendLog(sb, "query:" + httpRequest.getQueryString());
      appendLog(sb, "headers:" + requestHeaderString(httpRequest));
      ip = IpTool.getRequestIp(httpRequest);
    } else {
      ip = request.getRemoteAddr();
    }
    appendLog(sb, "address:" + ip + "," + request.getRemotePort());
    appendLog(sb, "encoding:" + request.getCharacterEncoding());
    appendLog(sb, "content-length:" + request.getContentLength());
    if (isHttpRequest) {
      appendLog(sb, "body:" + requestBodyString((MyServletRequestWrapper) request));
    }

    return sb.toString();
  }

  StringBuilder buildResponseLog(ServletResponse response, long requestStart) {
    StringBuilder sb = new StringBuilder(512);
    appendLog(sb, "Response<<<<<<");
    appendLog(sb, String.format("cost: %s ms", System.currentTimeMillis() - requestStart));
    if (response instanceof HttpServletResponse) {
      HttpServletResponse httpResponse = (HttpServletResponse) response;
      appendLog(sb, "headers:" + responseHeaderString(httpResponse));
      appendLog(sb, "status:" + httpResponse.getStatus());
    }
    appendLog(sb, "encoding:" + response.getCharacterEncoding());

    return sb;
  }

  private static String getBodyString(ContentCachingResponseWrapper response) {
    byte[] responseArray = response.getContentAsByteArray();
    return new String(responseArray);
  }

  private String buildResponseBodyLog(ContentCachingResponseWrapper response) {
    int len = response.getContentSize();
    StringBuilder sb = new StringBuilder(len + 32);
    appendLog(sb, "content-length:" + len);
    if (response.getContentSize() <= logFilterProperties.getMaxBodySize()) {
      appendLog(sb, "body:" + getBodyString(response));
    } else {
      appendLog(sb, "body:" + "...(body too long)");
    }
    return sb.toString();
  }

  private String requestBodyString(MyServletRequestWrapper request) {
    if (request.getContentLength() <= logFilterProperties.getMaxBodySize()) {
      return request.getBodyString();
    } else {
      return "...(body too long)";
    }
  }

  private static String responseHeaderString(HttpServletResponse response) {
    StringJoiner sj = new StringJoiner(", ", "(", ")");
    for (String h : response.getHeaderNames()) {
      sj.add(h + ": " + response.getHeaders(h));
    }
    return sj.toString();
  }

  private static String packHeaders(Enumeration<String> values) {
    StringJoiner sj = new StringJoiner(", ", "[", "]");
    while (values.hasMoreElements()) {
      sj.add(values.nextElement());
    }
    return sj.toString();
  }

  private static String requestHeaderString(HttpServletRequest request) {
    StringJoiner sj = new StringJoiner(", ", "(", ")");
    for (Enumeration<String> e = request.getHeaderNames(); e.hasMoreElements(); ) {
      String h = e.nextElement();
      sj.add(h + ": " + packHeaders(request.getHeaders(h)));
    }
    return sj.toString();
  }

  private boolean isMergedLog() {
    return logFilterProperties.getLogType() == LogType.MERGE;
  }

  private void httpLog(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Long start) throws IOException, ServletException {
    MyServletRequestWrapper requestWrapper = new MyServletRequestWrapper(request);
    String text = buildRequestLog(requestWrapper, true);
    boolean mergeLog = isMergedLog();
    if (!mergeLog) {
      log.info(text);
    }

    ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
    chain.doFilter(requestWrapper, responseWrapper);
    String body = buildResponseBodyLog(responseWrapper);
    responseWrapper.copyBodyToResponse();
    StringBuilder responseLog = buildResponseLog(responseWrapper, start);
    appendLog(responseLog, body);
    if (mergeLog) {
      text += logFilterProperties.getSeparator() + responseLog.toString();
    } else {
      text = responseLog.toString();
    }
    log.info(text);
  }

  private void noneHttpLog(ServletRequest request, ServletResponse response, FilterChain chain, Long start) throws IOException, ServletException {
    String text = buildRequestLog(request, false);
    boolean mergeLog = isMergedLog();
    if (!mergeLog) {
      log.info(text);
    }
    chain.doFilter(request, response);
    StringBuilder responseLog = buildResponseLog(response, start);
    if (mergeLog) {
      text += logFilterProperties.getSeparator() + responseLog.toString();
    } else {
      text = responseLog.toString();
    }
    log.info(text);
  }

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    Long start = System.currentTimeMillis();
    if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
      httpLog((HttpServletRequest) request, (HttpServletResponse) response, chain, start);
    } else {
      noneHttpLog(request, response, chain, start);
    }
  }
}
